﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using Affinity.Core.Exceptions;
using Affinity.Core.Utility;

namespace Affinity.Synthesis.ExpressionCells.Basis
{
    /// <summary>
    /// Expression cell representing a MethodCallExpression.
    /// </summary>
    internal class MethodInvocationCell : ExpressionCellBase
    {
        /// <summary>
        /// Factory method creating a MethodInvocationCell for a given MethodCallExpression (and its compiled delegate).
        /// </summary>
        /// <param name="methodCallExpression"></param>
        /// <param name="methodDelegate"></param>
        /// <returns></returns>
        public static MethodInvocationCell Create(MethodCallExpression methodCallExpression, Delegate methodDelegate)
        {
            if (methodCallExpression == null) throw new ArgumentNullException("methodCallExpression");

            MethodInfo method = methodCallExpression.Method;

            if ((method.DeclaringType == typeof(Enumerable) || method.DeclaringType == typeof(Queryable)) 
                && method.GetParameters().Length > 1)
                //&& method.GetParameters()[1].ParameterType.IsGenericType
                //&& method.GetParameters()[1].ParameterType.GetGenericTypeDefinition() == typeof(Func<,>))
            {
                if (method.Name == "Select") return OptimizedSelectExpressionCell.Create(methodCallExpression, methodDelegate);
                return new QueryMethodInvocationCell(methodCallExpression, methodDelegate);
            }
            else
            {
                return new MethodInvocationCell(methodCallExpression, methodDelegate);
            }
        }

        /// <summary>
        /// Constructor accepting the MethodCallExpression and its compiled delegate.
        /// </summary>
        /// <param name="methodCallExpression"></param>
        /// <param name="methodDelegate"></param>
        protected MethodInvocationCell(MethodCallExpression methodCallExpression, Delegate methodDelegate)
            : base(methodCallExpression)
        {
            if (methodCallExpression == null) throw new ArgumentNullException("methodCallExpression");
            if (methodCallExpression.Method == null) throw new ArgumentException("methodCallExpression.Method is null");

            _Method = methodCallExpression.Method;
            _IsExtensionMethod = _Method.GetCustomAttributes(typeof(ExtensionAttribute), false).Any();

            _MethodDelegate = methodDelegate;
        }

        /// <summary>
        /// Copy constructor.
        /// </summary>
        /// <param name="existing"></param>
        protected MethodInvocationCell(MethodInvocationCell existing)
            : base(existing)
        {
            _Method = existing._Method;
            _MethodDelegate = existing._MethodDelegate;
            _IsExtensionMethod = existing._IsExtensionMethod;
        }

        /// <summary>
        /// Creates and returns a copy of this expression cell.
        /// </summary>
        /// <returns></returns>
        public override IExpressionCell Copy()
        {
            return new MethodInvocationCell(this);
        }

        /*
        /// <summary>
        /// Returns a description of this expression cell.
        /// </summary>
        public override string ToString()
        {
            return string.Format("{0}: {1}.{2}", GetType().Name, _Method.DeclaringType.Name, _Method.Name);
        }
        */

        /// <summary>
        /// "Calculates" the value of this expression cell by invoking the compiled method delegate, passing the inputs as arguments.
        /// </summary>
        /// <param name="inputs"></param>
        /// <param name="hasValue"></param>
        /// <param name="value"></param>
        protected override void CalculateOutput(object[] inputs, out bool hasValue, out object value)
        {
            SetNotifyCollectionChangedSources(inputs.OfType<INotifyCollectionChanged>().ToArray());

            hasValue = true;

            if (!_Method.IsStatic || _IsExtensionMethod)
            {
                if (inputs.Length < 1) throw new Exception("Must have at least one input value.");
                hasValue = inputs[0] != null;
            }

            if (hasValue)
            {
                try
                {
                    value = _MethodDelegate.DynamicInvoke(inputs);
                }
                catch (Exception ex)
                {
                    ex = ExceptionUtil.GetInnermostException(ex);

                    throw new ExceptionWrapper(new ExceptionWrapper.Parameters
                        {
                            Message = string.Format("Exception encountered while executing expression {0}: {1}", Description, ex.Message),
                            StackTrace = ex.StackTrace
                        });
                }
            }
            else value = null;
        }

        /// <summary>
        /// Called when an input collection changes.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void HandleSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            OnValueDirty();
        }

        /// <summary>
        /// Specifies the given array of INotifyCollectionChanged inputs for monitoring for CollectionChanged events.
        /// When one of these collection changes, this expression cell's value is considered dirty.
        /// </summary>
        /// <param name="sources"></param>
        protected void SetNotifyCollectionChangedSources(INotifyCollectionChanged[] sources)
        {
            if (_WeakCollectionChangedCallback == null) _WeakCollectionChangedCallback = new WeakCollectionChangedCallback(this);

            foreach (var removedSource in _Sources.Except(sources)) _WeakCollectionChangedCallback.Remove(removedSource);
            foreach (var addedSource in sources.Except(_Sources)) _WeakCollectionChangedCallback.Add(addedSource);

            _Sources = sources;
        }

        private bool _IsExtensionMethod = false;
        private MethodInfo _Method = null;
        private Delegate _MethodDelegate = null;
        private INotifyCollectionChanged[] _Sources = new INotifyCollectionChanged[0];
        private WeakCollectionChangedCallback _WeakCollectionChangedCallback = null;

        /// <summary>
        /// Provides weak event handling for INotifyCollectionChanged.CollectionChanged events.
        /// </summary>
        private class WeakCollectionChangedCallback
        {
            /// <summary>
            /// Constructor accepting the owning MethodInvocationCell.
            /// </summary>
            /// <param name="cell"></param>
            public WeakCollectionChangedCallback(MethodInvocationCell cell)
            {
                _WeakCellReference = new WeakReference(cell);
            }

            /// <summary>
            /// Begins monitoring an INotifyCollectionChanged object's CollectionChanged event.
            /// </summary>
            /// <param name="notifyCollectionChanged"></param>
            public void Add(INotifyCollectionChanged notifyCollectionChanged)
            {
                notifyCollectionChanged.CollectionChanged += HandleCollectionChanged;
            }

            /// <summary>
            /// Ceases monitoring of an INotifyCollectionChanged object's CollectionChanged event.
            /// </summary>
            /// <param name="notifyCollectionChanged"></param>
            public void Remove(INotifyCollectionChanged notifyCollectionChanged)
            {
                notifyCollectionChanged.CollectionChanged -= HandleCollectionChanged;
            }

            /// <summary>
            /// Handles the CollectionChanged event. If the owning MethodInvocationCell has been garbage collected, 
            /// this method removes itself from the CollectionChanged event.
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            private void HandleCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
            {
                MethodInvocationCell strongCellReference = (MethodInvocationCell)_WeakCellReference.Target;
                if (strongCellReference == null) Remove((INotifyCollectionChanged)sender);
                else strongCellReference.HandleSourceCollectionChanged(sender, e);
            }

            private readonly WeakReference _WeakCellReference = null;
        }
    }
}
